// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package traffic

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"apiote.xyz/p/gott/v2"
	traffic_errors "apiote.xyz/p/szczanieckiej/traffic/errors"
	"git.sr.ht/~sircmpwn/go-bare"
	"golang.org/x/text/language"
)

type DepartureRealtimeNew struct {
	Departure
	Order     StopOrder
	Update    Update
	Time      time.Time
	Alerts    []SpecificAlert
	startTime *uint
}

func (d DepartureRealtimeNew) IsEmpty() bool {
	return d.Time.IsZero()
}

func (d DepartureRealtimeNew) WithUpdate(update Update) DepartureRealtimeNew {
	d.Update = update
	return d
}

func (d DepartureRealtimeNew) WithAlerts(alerts []Alert, languages []language.Tag) DepartureRealtimeNew {
	d.Alerts = selectSpecificAlerts(alerts, languages)
	return d
}

func (d DepartureRealtimeNew) GetTimeWithDelay() time.Time {
	if d.Update.TimeUTC != "" {
		updateTimeUTC, err := time.Parse("150405", d.Update.Time)
		if err != nil {
			panic("departure update time ‘" + d.Update.Time + "’ not in format 150405")
		}
		updateTime := time.Date(d.Time.Year(), d.Time.Month(), d.Time.Day(), updateTimeUTC.Hour(), updateTimeUTC.Minute(), updateTimeUTC.Second(), 0, time.UTC)
		return updateTime.In(d.Time.Location())
	} else if d.Update.Time != "" {
		updateTime, err := time.Parse("150405", d.Update.Time)
		if err != nil {
			panic("departure update time ‘" + d.Update.Time + "’ not in format 150405")
		}
		updateDateTime := time.Date(d.Time.Year(), d.Time.Month(), d.Time.Day(), updateTime.Hour(), updateTime.Minute(), updateTime.Second(), 0, d.Time.Location())
		return updateDateTime
	} else {
		delay := int(d.Update.Delay)
		return d.Time.Add(time.Duration(delay) * time.Second)
	}
}

type DeparturesResult struct {
	traffic        *Traffic
	context        Context
	date           time.Time
	timetableHome  string
	calendar       []Schedule
	stopOffset     uint
	languages      []language.Tag
	departuresType DeparturesType
	lineID         string

	timezone         *time.Location
	datetime         time.Time
	minuteB4Datetime time.Time
	scheduleIDs      map[string]map[string]struct{}
	stopsFile        *os.File
	stop             Stop
	tripsFile        *os.File
	trips            map[string][]Trip
	feedInfo         FeedInfo
	enrichMethod     func(string, int, string, string, Context) (map[string][]Update, map[string][]Alert, bool, error)

	departures []DepartureRealtimeNew
}

func (r DeparturesResult) getTraffic() *Traffic {
	return r.traffic
}
func (r DeparturesResult) getContext() Context {
	return r.context
}

func (r *DeparturesResult) setTimezone(l *time.Location) {
	r.timezone = l
}

func (r *DeparturesResult) setStopsFile(f *os.File) {
	r.stopsFile = f
}

func (r DeparturesResult) getTimetableHome() string {
	return r.timetableHome
}

func (r DeparturesResult) getStopsFile() *os.File {
	return r.stopsFile
}

func (r DeparturesResult) getStopOffset() uint {
	return r.stopOffset
}

func (r *DeparturesResult) setStop(s Stop) {
	r.stop = s
}

func (r *DeparturesResult) setTripsFile(f *os.File) {
	r.tripsFile = f
}

func (r DeparturesResult) getTripsFile() *os.File {
	return r.tripsFile
}

func loadTime(r TrafficResult) TrafficResult {
	result := r.(*DeparturesResult)

	now := time.Now()
	datetime := time.Date(result.date.Year(), result.date.Month(),
		result.date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(result.timezone)
	result.datetime = datetime
	result.minuteB4Datetime = datetime.Add(time.Duration(-1) * time.Minute)
	return result
}

func loadSchedules(r TrafficResult) (TrafficResult, error) {
	result := r.(*DeparturesResult)

	schedules := map[string]map[string]struct{}{}
	for _, offset := range []int{0, -1} {
		date := result.date.AddDate(0, 0, offset)
		scheduleIDs, err := findSchedule(result.timetableHome, date, result.calendar)
		if err != nil {
			log.Printf("no schedule for %s: %v\n", date, err)
		} else {
			schedules[date.Format(DateFormat)] = scheduleIDs
		}
	}

	result.scheduleIDs = schedules
	if len(schedules) == 0 {
		return result, fmt.Errorf("no schedules found")
	}
	return result, nil
}

func selectTrips(r TrafficResult) (TrafficResult, error) {
	result := r.(*DeparturesResult)
	trips := map[string][]Trip{}

	for _, order := range result.stop.Order {
		_, err := result.tripsFile.Seek(int64(order.TripOffset), 0)
		if err != nil {
			return result, fmt.Errorf("while seeking to StopOrder %v: %w", order, err)
		}
		trip := Trip{}
		err = bare.UnmarshalReader(result.tripsFile, &trip)
		if err != nil {
			return result, fmt.Errorf("while unmarshalling trip at offset %d: %w", order.TripOffset, err)
		}

		for startDate, scheduleIDs := range result.scheduleIDs {
			if _, ok := scheduleIDs[trip.ScheduleID]; ok {
				trips[startDate] = append(trips[startDate], trip)
			}
		}
	}

	result.trips = trips
	result.tripsFile.Close()
	return result, nil
}

func getFeedInfo2(r TrafficResult) (TrafficResult, error) {
	result := r.(*DeparturesResult)
	feedInfo, err := getFeedInfo(result.context.DataHome, result.context.FeedID, result.context.Version)
	result.feedInfo = feedInfo
	return result, err
}

func selectEnrichMethod(r TrafficResult) TrafficResult {
	result := r.(*DeparturesResult)
	if result.feedInfo.Name != "" {
		_, tripUpdatesRtFeed := result.feedInfo.RealtimeFeeds[TRIP_UPDATES]
		_, vehiclePositionsRtFeed := result.feedInfo.RealtimeFeeds[VEHICLE_POSITIONS]
		if tripUpdatesRtFeed || vehiclePositionsRtFeed {
			result.enrichMethod = getGtfsRealtimeUpdates
			// log.Println("GTFS")
		} else if isLuaUpdatesScript(result.context) {
			result.enrichMethod = getLuaRealtimeUpdates
			// log.Println("Lua")
		} else {
			result.enrichMethod = nil
			// log.Println("none")
		}
	} else {
		result.enrichMethod = nil
		// log.Println("else")
	}
	return result
}

func getNoTripsDepartures(r TrafficResult) (TrafficResult, error) {
	result := r.(*DeparturesResult)
	if !isLuaUpdatesScript(result.context) {
		return r, nil
	}

	updates, alerts, areTripsInTimetable, err := result.enrichMethod("", -1, result.stop.Id, result.stop.Code, result.context)

	if areTripsInTimetable {
		return r, nil
	}

	if err != nil {
		return r, fmt.Errorf("while getting departures: %w", err)
	}

	pickups := map[string]Boarding{}
	dropoffs := map[string]Boarding{}
	for _, trips := range result.trips {
		for _, trip := range trips {
			if _, ok := pickups[trip.LineID]; ok {
				continue
			}
			for _, d := range trip.Departures {
				if d.StopSequence == result.stop.Order[trip.Id].Sequence {
					pickups[trip.LineID] = d.Pickup
					dropoffs[trip.LineID] = d.Dropoff
				}
			}
		}
	}

	departures, err := makeDeparturesFromNoTripUpdates(updates[result.stop.Code], alerts, pickups, dropoffs, result.timezone, result.languages)
	if err != nil {
		return r, fmt.Errorf("while creating departures without trip: %w", err)
	}

	result.departures = departures
	return result, nil
}

func makeDeparturesFromNoTripUpdates(updates []Update, alerts map[string][]Alert, pickups, dropoffs map[string]Boarding, timezone *time.Location, languages []language.Tag) ([]DepartureRealtimeNew, error) {
	departures := []DepartureRealtimeNew{}
	now := time.Now().In(timezone)
	for _, update := range updates {
		if update.Time == "" {
			log.Printf("update time is empty, update is %+v\n", update)
			continue
		}
		departureTime, err := time.Parse("150405", update.Time)
		if err != nil {
			return departures, fmt.Errorf("while parsing time: %w", err)
		}

		update.Delay = 0
		departureTime = time.Date(now.Year(), now.Month(), now.Day(), departureTime.Hour(), departureTime.Minute(), departureTime.Second(), 0, timezone)
		departures = append(departures, DepartureRealtimeNew{
			Time: departureTime,
			Departure: Departure{
				Pickup:  pickups[update.VehicleStatus.LineID],
				Dropoff: dropoffs[update.VehicleStatus.LineID],
			},
			Order: StopOrder{
				uint(departureTime.Unix()),
				0,
			},
			Update: update,
			Alerts: selectSpecificAlerts(alerts[update.VehicleStatus.TripID], languages),
		})
	}

	return departures, nil
}

func getTripsDepartures(r TrafficResult) (TrafficResult, error) {
	result := r.(*DeparturesResult)
	departures := []DepartureRealtimeNew{}

	if len(result.departures) > 0 {
		return r, nil
	}

	var ber BlockingError
	timedOut := false

	for startDate, trips := range result.trips {
		for _, trip := range trips {
			tripStartDate, _ := time.Parse(DateFormat, startDate) // NOTE internally formatted date, will alway succeed
			if len(trip.Headways) == 0 {                          // NOTE departures based on schedule
				scheduleDeparture, err := getScheduleDeparture(trip, result.stop.Order[trip.Id], tripStartDate, result.timezone, nil, result.context)
				if err != nil {
					log.Printf("no departures found for trip %s, order %v\n", trip.Id, result.stop.Order[trip.Id])
					continue
				}

				if result.enrichMethod != nil && !timedOut {
					departure, err := enrichDeparture(result, scheduleDeparture[0], trip)
					if err != nil {
						if isTimeout(err) || errors.As(err, &ber) || strings.Contains(err.Error(), "connection refused") { // TODO or any other connection problem
							timedOut = true
							log.Printf("blocking error while enriching departure %s -> %s (%v): %v", trip.LineID, trip.Headsign, departure.Time, err)
						} else {
							log.Printf("while enriching departure %s -> %s (%v): %v\n", trip.LineID, trip.Headsign, departure.Time, err)
						}
						departures = append(departures, scheduleDeparture[0])
					} else {
						departures = append(departures, departure)
					}
				} else {
					departures = append(departures, scheduleDeparture[0])
				}
			} else { // departures based on frequencies
				for _, headway := range trip.Headways {
					if headway.Exact { // departures based on frequencies; exact times
						headwayDepartures, err := getScheduleDeparture(trip, result.stop.Order[trip.Id], tripStartDate, result.timezone, &headway, result.context)
						if err != nil {
							log.Printf("no departures found for trip %s, order %v\n", trip.Id, result.stop.Order[trip.Id])
							break
						}
						for _, headwayDeparture := range headwayDepartures {
							if result.enrichMethod != nil && !timedOut {
								departure, err := enrichDeparture(result, headwayDeparture, trip)
								if err != nil {
									if isTimeout(err) || errors.As(err, &ber) || strings.Contains(err.Error(), "connection refused") { // TODO or any other connection problem
										timedOut = true
										log.Printf("blocking error while enriching departure %s -> %s (%v): %v", trip.LineID, trip.Headsign, departure.Time, err)
									} else {
										log.Printf("while enriching departure %s -> %s (%v): %v\n", trip.LineID, trip.Headsign, departure.Time, err)
									}
									departures = append(departures, headwayDeparture)
								} else {
									departures = append(departures, departure)
								}
							} else {
								departures = append(departures, headwayDeparture)
							}
						}
					} else { // departures based on frequencies; inexact times
						headwayDepartures, err := getScheduleDeparture(trip, result.stop.Order[trip.Id], tripStartDate, result.timezone, &headway, result.context)
						if err != nil {
							log.Printf("no departures found for trip %s, order %v\n", trip.Id, result.stop.Order[trip.Id])
							break
						}
						var headwayUpdates []DepartureRealtimeNew
						if !timedOut {
							headwayUpdates, err = getUpdatesDepartures(result, trip, headwayDepartures[0].Pickup, headwayDepartures[0].Dropoff, false, tripStartDate, headwayDepartures[0].Departure.Time)
							if err != nil {
								if isTimeout(err) || errors.As(err, &ber) || strings.Contains(err.Error(), "connection refused") { // TODO or any other connection problem
									timedOut = true
									log.Printf("blocking error while getting updates departures: %v", err)
								} else {
									log.Printf("blocking error while getting updates departures: %v", err)
								}
							}
						}
						headwayUpdatesNumber := len(headwayUpdates)
						if headwayUpdatesNumber == 0 {
							departures = append(departures, headwayDepartures...)
						} else {
							departures = append(departures, headwayUpdates...)
							templateDeparture := headwayUpdates[0]
							for i := uint(0); ; i++ {
								singleDepartureTime := templateDeparture.Time.Add(time.Duration(headway.Interval*i) * -time.Second)
								if singleDepartureTime.Before(headwayDepartures[0].Time) {
									break
								}
								singleDeparture := templateDeparture
								singleDeparture.Update = Update{
									VehicleStatus: VehicleStatus{
										LineID:   trip.LineID,
										Headsign: trip.Headsign,
									},
								}
								singleDeparture.Time = singleDepartureTime
								departures = append(departures, singleDeparture)
							}
							templateDeparture = headwayUpdates[headwayUpdatesNumber-1]
							for i := uint(0); ; i++ {
								singleDepartureTime := templateDeparture.Time.Add(time.Duration(headway.Interval*i) * time.Second)
								if singleDepartureTime.After(headwayDepartures[len(headwayDepartures)-1].Time) {
									break
								}
								singleDeparture := templateDeparture
								singleDeparture.Update = Update{
									VehicleStatus: VehicleStatus{
										LineID:   trip.LineID,
										Headsign: trip.Headsign,
									},
								}
								singleDeparture.Time = singleDepartureTime
								departures = append(departures, singleDeparture)
							}
						}
					}
				}
			}
		}
	}

	result.departures = departures
	return result, nil
}

func getScheduleDeparture(trip Trip, order StopOrder, tripStartDate time.Time, timezone *time.Location, headway *Headway, context Context) ([]DepartureRealtimeNew, error) {
	templateDeparture := DepartureRealtimeNew{}
	found := false
	for _, departure := range trip.Departures {
		if departure.StopSequence == order.Sequence {
			templateDeparture.Departure = departure
			templateDeparture.Order = order
			templateDeparture.Update = Update{
				VehicleStatus: VehicleStatus{
					Headsign: trip.Headsign,
					LineID:   trip.LineID,
				},
			}
			templateDeparture.Exact = departure.Exact

			templateDeparture.Time = calculateGtfsTime(departure.Time, 0, tripStartDate,
				timezone)
			found = true
			break
		}
	}
	if !found {
		return []DepartureRealtimeNew{}, traffic_errors.NoStopOrder{
			TripID: trip.Id,
			Order:  order.Sequence,
		}
	}

	if headway == nil {
		return []DepartureRealtimeNew{templateDeparture}, nil
	}

	departures := []DepartureRealtimeNew{}
	for i := uint(0); ; i++ {
		if headway.StartTime+(headway.Interval*i) > headway.EndTime {
			break
		}
		singleDeparture := templateDeparture
		singleDeparture.Exact = (headway.Exact && templateDeparture.Exact)
		singleDeparture.Time = singleDeparture.Time.Add(time.Duration(headway.StartTime) * time.Second).Add(time.Duration(headway.Interval*i) * time.Second)
		startTime := templateDeparture.Departure.Time + headway.StartTime + (headway.Interval * i)
		singleDeparture.startTime = &startTime
		departures = append(departures, singleDeparture)
	}
	return departures, nil
}

func enrichDeparture(r *DeparturesResult, scheduleDeparture DepartureRealtimeNew, trip Trip) (DepartureRealtimeNew, error) {
	if r.departuresType == DEPARTURES_HYBRID {
		var (
			updates map[string][]Update
			alerts  map[string][]Alert
			err     error
		)
		updates, alerts, _, err = r.enrichMethod(trip.Id, scheduleDeparture.Order.Sequence, r.stop.Id, r.stop.Code, r.context)
		if err != nil {
			return scheduleDeparture, err
		}
		tripUpdates := updates[trip.Id]
		var validTripUpdate Update
		for _, tripUpdate := range tripUpdates {
			if scheduleDeparture.startTime != nil && tripUpdate.StartTime != nil && *scheduleDeparture.startTime != *tripUpdate.StartTime {
				// TODO if update.relationship is UNSCHEDULED -> add this update as new departure
				continue
			}
			if tripUpdate.StopSequence > uint32(scheduleDeparture.Order.Sequence) {
				break
			}
			validTripUpdate.Time = tripUpdate.Time
			validTripUpdate.Delay = tripUpdate.Delay
			validTripUpdate.StopID = tripUpdate.StopID
			validTripUpdate.StopSequence = tripUpdate.StopSequence
			validTripUpdate.TimetableRelationship = tripUpdate.TimetableRelationship
			validTripUpdate.VehicleStatus = tripUpdate.VehicleStatus
		}
		validTripUpdate.VehicleStatus.LineID = trip.LineID
		validTripUpdate.VehicleStatus.Headsign = trip.Headsign
		departure := scheduleDeparture.WithUpdate(validTripUpdate).WithAlerts(alerts[trip.Id], r.languages)
		return departure, nil
	} else {
		update := Update{}
		update.VehicleStatus.LineID = trip.LineID
		update.VehicleStatus.Headsign = trip.Headsign
		return scheduleDeparture.WithUpdate(update), nil
	}
}

func getUpdatesDepartures(r *DeparturesResult, trip Trip, pickup, dropoff Boarding, exact bool, tripStartDate time.Time, departureTime uint) ([]DepartureRealtimeNew, error) {
	departures := []DepartureRealtimeNew{}
	updates, alerts, _, err := r.enrichMethod(trip.Id, r.stop.Order[trip.Id].Sequence, r.stop.Id, r.stop.Code, r.context)
	if err != nil {
		return departures, err
	}
	tripUpdates := updates[trip.Id]
	updatesMap := map[uint]Update{}
	for _, tripUpdate := range tripUpdates {
		if tripUpdate.StopSequence > uint32(r.stop.Order[trip.Id].Sequence) {
			break
		}
		if tripUpdate.StartTime == nil {
			log.Printf("invalid update for frequency-based inexact-time trip: without start time")
		}
		validTripUpdate := updatesMap[*tripUpdate.StartTime]
		validTripUpdate.Time = tripUpdate.Time
		validTripUpdate.Delay = tripUpdate.Delay
		validTripUpdate.StopID = tripUpdate.StopID
		validTripUpdate.StopSequence = tripUpdate.StopSequence
		validTripUpdate.TimetableRelationship = tripUpdate.TimetableRelationship
		validTripUpdate.VehicleStatus = tripUpdate.VehicleStatus
		validTripUpdate.VehicleStatus.LineID = trip.LineID
		validTripUpdate.VehicleStatus.Headsign = trip.Headsign
		updatesMap[*tripUpdate.StartTime] = validTripUpdate
	}
	for _, validTripUpdate := range updatesMap {
		scheduleDeparture := DepartureRealtimeNew{
			Departure: Departure{
				StopSequence: r.stop.Order[trip.Id].Sequence,
				Pickup:       pickup,
				Dropoff:      dropoff,
				Exact:        exact,
			},
			Order:     r.stop.Order[trip.Id],
			Update:    validTripUpdate,
			Time:      calculateGtfsTime(*validTripUpdate.StartTime, 0, tripStartDate, r.timezone).Add(time.Duration(departureTime) * time.Second),
			startTime: validTripUpdate.StartTime,
		}
		departure := scheduleDeparture.WithUpdate(validTripUpdate).WithAlerts(alerts[trip.Id], r.languages)
		departures = append(departures, departure)
	}
	return departures, nil
}

func dropTrips(r TrafficResult) TrafficResult {
	result := r.(*DeparturesResult)
	result.trips = map[string][]Trip{}
	return result
}

func filterDepartures(r TrafficResult) TrafficResult {
	result := r.(*DeparturesResult)
	departures := []DepartureRealtimeNew{}
	midnight := result.date // TODO should be in client timezone
	for _, departure := range result.departures {
		if (result.departuresType == DEPARTURES_FULL && departure.GetTimeWithDelay().After(midnight)) || (result.departuresType == DEPARTURES_HYBRID && departure.GetTimeWithDelay().After(result.minuteB4Datetime)) {
			departures = append(departures, departure)
		}
	}
	result.departures = departures
	return result
}

func filterDeparturesByLine(r TrafficResult) TrafficResult {
	result := r.(*DeparturesResult)
	departures := []DepartureRealtimeNew{}
	if result.lineID != "" {
		for _, departure := range result.departures {
			if departure.Update.VehicleStatus.LineID == result.lineID {
				departures = append(departures, departure)
			}
		}
		result.departures = departures
	}
	return result
}

func addAlerts(r TrafficResult) TrafficResult {
	result := r.(*DeparturesResult)
	alertedDepartures := make([]DepartureRealtimeNew, len(result.departures))
	for i, d := range result.departures {
		if len(d.Alerts) == 0 {
			d.Alerts = GetAlerts("", "", int(d.Order.TripOffset), result.context, result.traffic, result.languages)
		}
		alertedDepartures[i] = d
	}
	result.departures = alertedDepartures
	return result
}

func sortDepartures(r TrafficResult) TrafficResult {
	result := r.(*DeparturesResult)
	sort.Slice(result.departures, func(i, j int) bool {
		return result.departures[i].GetTimeWithDelay().Before(result.departures[j].GetTimeWithDelay())
	})

	return result
}

func GetDepartures(stopCode, lineID string, ctx Context, traffic *Traffic, date time.Time, departuresType DeparturesType, languages []language.Tag) ([]DepartureRealtimeNew, error) {

	codeIndex := traffic.CodeIndexes[ctx.FeedID][ctx.Version]
	calendar := traffic.Calendars[ctx.FeedID][ctx.Version]

	result := &DeparturesResult{
		traffic:        traffic,
		context:        ctx,
		date:           date, // has timezone of feed
		timetableHome:  filepath.Join(ctx.DataHome, ctx.FeedID, string(ctx.Version)),
		calendar:       calendar,
		stopOffset:     codeIndex[stopCode],
		languages:      languages,
		departuresType: departuresType,
		lineID:         lineID,
	}

	r := gott.R[TrafficResult]{S: result}.
		Bind(loadTimezone).
		Map(loadTime).
		Bind(loadSchedules).
		Bind(openStopsFile).
		Bind(seekStopsFile).
		Bind(unmarshallStop).
		Bind(openTripsFile).
		Bind(selectTrips).
		Bind(getFeedInfo2).
		Map(selectEnrichMethod).
		Bind(getNoTripsDepartures).
		Bind(getTripsDepartures).
		Map(dropTrips).
		Map(filterDepartures).
		Map(filterDeparturesByLine).
		Map(addAlerts).
		Map(sortDepartures).
		Recover(closeFiles)

	return r.S.(*DeparturesResult).departures, r.E
}
